package DatagramPlusPlus;


import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.*;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Random;

/**
 *
 * @author Rafael
 */
public class DatagramPlusPlusSocket  extends SocketImpl {

    // Controlo do Socket
    protected DatagramSocket socket;
    private DatagramReceiver receiver;
    private KeepingAlive keepingAlive;

    // Transmissao de dados
    private InputStream in;
    private OutputStream out;
    private LinkedList<Byte> proximos;
    private LinkedList<Byte> aEnviar;


    // Controlo de congestao
    protected HashMap<Long,Long> waitingAck;
    protected Long timeOut;
    protected Integer windowSize = 8;
    protected Integer currentWindow = 0;

//    private boolean aEscuta

    // <editor-fold defaultstate="collapsed" desc="Construtores">
    /**
     * Cria um socket sem ligacao
     * @throws SocketException
     * /
    public DatagramPlusPlusSocket() throws SocketException {
        super();
    }
    /**
     * Cria um socket e poe a escuta na porta port
     * @param port porta a escutar
     * @throws SocketException
     */
    protected DatagramPlusPlusSocket(int port) throws SocketException {
        super();
        Random random = new Random();
        localport = port;
        super.port = random.nextInt(65535-1024)+2014;
        address = null;
        socket = new DatagramSocket(port);
    }

    /**
     * Cria um socket e conecta com o endereco address na porta port
     * @param address ip a conectar o socket
     * @param port porta destino
     * @throws SocketException
     */
    public DatagramPlusPlusSocket(InetAddress address, int port) throws SocketException {
        super();
        super.port = port;
        super.address = address;
        socket = new DatagramSocket();
        estabelecerLigacao();
    }

    private void estabelecerLigacao() throws SocketException {
        
        try {
            DatagramPlusPlusPacket init =
                new DatagramPlusPlusPacket(new DatagramType(DatagramType.INIT));
            init.setAddress(address);
            init.setPort(port);
            socket.send(init.getUDPPacket());
        } catch (IOException ex) {
            System.out.println("Inicio de ligacao falhou: Nao foi possivel solicitar ligacao.");
            SocketException exc = new SocketException();
            throw exc;
        }
            System.out.println("Solicitando ligacao com "+address+" na porta "+port+".");

            DatagramPlusPlusPacket initReply =
                    new DatagramPlusPlusPacket(new DatagramType(DatagramType.INIT_REPLY));
        try {
            socket.receive(initReply.getUDPPacket());
        } catch (IOException ex) {
            System.out.println("Inicio de ligacao falhou: Nao foi possivel receber confirmacao.");
            SocketException exc = new SocketException();
            throw exc;
        }

        initReply.refreshVars();
        if(initReply.getTipo().getTipo() == DatagramType.INIT_REPLY)    {

            DatagramPlusPlusPacket ack = new Ack(initReply);
            try {
                socket.send(ack.getUDPPacket());
            } catch (IOException ex) {
                System.out.println("Inicio de ligacao falhou: Nao foi possivel confirmar ligacao.");
                SocketException exc = new SocketException();
                throw exc;
            }

                System.out.println("Ligacao estabelecida com sucesso.\n");

                // Iniciar Receiver
                keepingAlive = new KeepingAlive(this, receiver, socket);
                receiver = new DatagramReceiver(socket,this,keepingAlive);
                Thread thread = new Thread(receiver);
                thread.start();
                // Iniciar KeepingAlive
                Thread thread1 = new Thread(keepingAlive);
                thread1.start();

        } else  {
                System.out.println("Inicio de ligacao falhou: Tipo de pacote errado.");
                SocketException exc = new SocketException();
                throw exc;
        }
        
    }
    // </editor-fold>


    // <editor-fold defaultstate="collapsed" desc="API Socket">
    /**
     * Accepts a connection.
     * @param s
     */
    protected void accept(SocketImpl s) throws IOException {
        IOException ex = new IOException("Este socket nao aceita novas conexoes");
        throw ex;
    }
    /**
     * Returns the number of bytes that can be read from this socket without blocking.
     * @return
     */
    public int available() {
        int size = 0;
        for(DatagramPlusPlusPacket packet : receiver.buffer)    {
            size += packet.getData().length;
        }
        return size;
    }
    /**
     * Binds this socket to the specified local IP address and port number.
     * @param host
     * @param port
     */
    public void bind(InetAddress host, int port) throws IOException {
        IOException ex = new IOException("Nao implementado");
        throw ex;
    }
    /**
     * Closes this socket.
     */
    public void close()  {
        DatagramPlusPlusPacket packet =
                new DatagramPlusPlusPacket(new DatagramType(DatagramType.KILL));
        // Desativar receiver
        receiver.recebendo = false;
        try {
            // Enviar pacotes KILL
            socket.send(packet.getUDPPacket());
        } catch (IOException ex) {
            System.err.println("Falha ao enviar KILL.");
        }
        synchronized(timeOut)   {
            WaitAndKill wKill = new WaitAndKill(timeOut);
            Thread thr = new Thread(wKill);
            thr.start();
        }
        // Receber KILL-ACK
        do{
            try {
                socket.receive(packet.getUDPPacket());
            } catch (IOException ex) {
                System.err.println("Falha ao receber ACK-KILL.");
            }
            packet.refreshVars();
        }while(packet.getTipo().getTipo()!=DatagramType.KILL && packet.getFlagAck());
        System.out.println("Conexao Terminada.");

    }
    /**
     * Connects this socket to the specified port number on the specified host.
     * @param address
     * @param port
     */
    protected void connect(InetAddress address, int port) throws IOException   {
        IOException ex = new IOException("Nao implementado");
        throw ex;
    }
    /**
     * Connects this socket to the specified port number on the specified host.
     * @param address
     * @param timeout
     */
    protected void connect(SocketAddress address, int timeout) throws IOException  {
        IOException ex = new IOException("Nao implementado");
        throw ex;
    }
    /**
     * Connects this socket to the specified port on the named host.
     * @param host
     * @param port
     */
    protected void connect(String host, int port) throws IOException   {
        IOException ex = new IOException("Nao implementado");
        throw ex;
    }
    /**
     * Creates either a stream or a datagram socket.
     * @param stream
     */
    protected void create(boolean stream) throws IOException   {
        IOException ex = new IOException("Nao implementado");
        throw ex;
    }
    /**
     * Returns the value of this socket's fd field.
     * @return
     */
    protected FileDescriptor getFileDescriptor() {
        return super.fd;
    }
    /**
     * Returns an input stream for this socket.
     * @return
     */
    public InputStream getInputStream()  {
        in = new InputStream() {
            
            @Override
            public int read() throws IOException {
                while (true) {
                    if(proximos.size()>0)   {
                        return proximos.removeFirst();
                    }
                    synchronized(receiver.buffer)   {
                        
                        if(receiver.buffer.size() > 0)  {
                            byte[] data = receiver.buffer.removeLast().getData();
                            for(int i=data.length-1 ; i>=0 ; i--)
                                proximos.addFirst(data[i]);
                            return proximos.removeFirst();
                        }
                    }
                    try {
                        this.wait();
                    } catch (InterruptedException ex) {
                        System.err.println(ex.getMessage());
                    }
                }
            }
        };
        return in;
    }
    /**
     * Returns the value of this socket's localport field.
     * @return
     */
    @Override
    public int getLocalPort()    {
        return localport;
    }
    public boolean notClosed()  {
        return !socket.isClosed();
    }
    /**
     * Returns an output stream for this socket.
     * @return
     */
    public OutputStream getOutputStream()    {
        out = new OutputStream() {
            @Override
            public void write(int b) throws IOException {
                // Se buffer estiver pronto para enviar
                if(aEnviar.size()>=1024)    {
                    // Prepara dados
                    byte[] buf = new byte[1024];
                    for(int i=0 ; i<1024 ; i++)
                        buf[i]=aEnviar.removeFirst();
                    // Se houver janela
                    while(currentWindow <= 0)
                        try {
                        this.wait();
                    } catch (InterruptedException ex) {
                        System.err.print("Falha ao enviar pacote de dados.");
                    }
                    // Envia
                    DatagramPlusPlusPacket packet =
                            new DatagramPlusPlusPacket(buf, buf.length, address, port);
                    socket.send(packet.getUDPPacket());
                    System.out.println("A enviar pacote nr "+packet.getNumSerie());
                    waitingAck.put(packet.getNumSerie(),System.nanoTime());

                }
                Integer i = new Integer(b);
                aEnviar.add(i.byteValue());
            }
            @Override
            public void flush() throws IOException {
                // Prepara dados
                    byte[] buf = new byte[1024];
                    for(int i=0 ; i<1024 ; i++)
                        buf[i]=aEnviar.removeFirst();
                    // Se houver janela
                    while(currentWindow <= 0)
                        try {
                        this.wait();
                    } catch (InterruptedException ex) {
                        System.err.print("Falha ao enviar pacote de dados.");
                    }
                    // Envia
                    DatagramPlusPlusPacket packet =
                            new DatagramPlusPlusPacket(buf, buf.length, address, port);
                    socket.send(packet.getUDPPacket());
                    System.out.println("A enviar pacote nr "+packet.getNumSerie());
                    waitingAck.put(packet.getNumSerie(),System.nanoTime());
            }
        };
        return out;
    }
    /**
     * Returns the value of this socket's port field.
     * @return
     */
    public int getPort() {
        return port;
    }
    /**
     * Sets the maximum queue length for incoming connection indications (a request to connect) to the count argument.
     * @param backlog
     */
    protected void listen(int backlog) throws IOException  {
        IOException ex = new IOException("Nao implementado");
        throw ex;
    }
    /**
     * Send one byte of urgent data on the socket.
     * @param data
     */
    protected void sendUrgentData(int data) throws IOException {
        IOException ex = new IOException("Nao implementado");
        throw ex;
    }

    public void setOption(int optID, Object value) throws SocketException {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public Object getOption(int optID) throws SocketException {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    /**
     * @return the socket
     */
    protected DatagramSocket getUDPSocket() {
        return socket;
    }

    // </editor-fold>

}
